﻿/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping.Interface;

namespace Nvigorate.Relational.Mapping.Index
{
    public enum StandardPaths
    {
        None,
        ExecutableRoot
    }

    [Serializable]
    public class MapIndex : IMapIndex, ISerializable
    {
        #region Private Fields

        protected static Dictionary<string, Dictionary<Type, IBindingMap>> _mapLookup =
            new Dictionary<string, Dictionary<Type, IBindingMap>>();

        #endregion

        #region Properties

        public IBindingMap this[string repositoryName, Type type]
        {
            get { return repositoryName != "" ? GetFinalMap(_mapLookup[repositoryName][type]) : GetDefaultMap(type); }
            set
            {
                if (!_mapLookup.ContainsKey(repositoryName))
                    _mapLookup[repositoryName] = new Dictionary<Type, IBindingMap>();
                _mapLookup[repositoryName][type] = value;
            }
        }

        #endregion

        #region IMapIndex Members

        /// <summary>
        /// Adds the binding map to the index
        /// </summary>
        /// <param name="bindingMap">The map to add</param>
        public void AddBindingMap(IBindingMap bindingMap)
        {
            this[bindingMap.RepositoryName, bindingMap.TargetType] = bindingMap;
        }

        /// <summary>
        /// Adds the collection of maps to the index
        /// </summary>
        /// <param name="maps">The maps to add</param>
        public void AddBindingMaps(IEnumerable<IBindingMap> maps)
        {
            maps.ForEach(AddBindingMap);
        }

        /// <summary>
        /// Remove all maps from lookup
        /// </summary>
        public void ClearMaps()
        {
            _mapLookup.Clear();    
        }

        /// <summary>
        /// Determines whether [has binding map] [the specified type].
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="repositoryName">Name of the source.</param>
        /// <returns>
        /// 	<c>true</c> if [has binding map] [the specified type]; otherwise, <c>false</c>.
        /// </returns>
        public bool HasBindingMap(Type type, string repositoryName)
        {
            return _mapLookup.ContainsKey(repositoryName) ? _mapLookup[repositoryName].ContainsKey(type) : false;
        }

        public IEnumerable<ITargetRelationship> GetRelationshipsForType(Type type, string repositoryName)
        {
            return
                _mapLookup[repositoryName].SelectMany(m => m.Value.Relationships).Where(
                    r => r.SubjectType.Equals(type) || r.RelativeType.Equals(type));
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Default constructor.  This should only be used by MapIndexFactory.  Consuming code 
        /// should call MapIndexFactory.GetMapIndex.
        /// </summary>
        internal MapIndex(){}

        public MapIndex(SerializationInfo info, StreamingContext context)
        {
            _mapLookup =
                (Dictionary<string, Dictionary<Type, IBindingMap>>)
                info.GetValue("mapLookup", typeof (Dictionary<string, Dictionary<Type, IBindingMap>>));
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Builds a default map where the record source and object type match up
        /// </summary>
        /// <returns></returns>
        private IBindingMap GetDefaultMap(Type targetType)
        {
            var map = new BindingMap();

            map.AddAssignments(
                Reflector
                    .GetProperties(targetType)
                    .Where(p => Reflector.GetPropertyInfo(targetType, p, true).CanWrite)
                    .Select(p => BuildMemberAssignment(false, targetType.Name, targetType, p, p))
                    .Cast<IAssignment>());

            return map;
        }


        private IBindingMap GetFinalMap(IBindingMap map)
        {
            var parents = Reflector.GetInheritenceChain(map.TargetType);
            if (parents != null)
            {
                parents
                    .Where(p => HasBindingMap(p, map.RepositoryName))
                    .ForEach(p => map.Merge(this[map.RepositoryName, p]));
            }

            //Merge children up to this map
            if(map.Discriminator != null)
            {
                var childMaps = _mapLookup[map.RepositoryName].Keys
                    .Where(k => Reflector.HasBaseTypeOf(k, map.TargetType) && !k.Equals(map.TargetType));
                childMaps
                    .ForEach(c => map.Merge(_mapLookup[map.RepositoryName][c]));
            }

            return map;
        }

        /// <summary>
        /// Method which generates a 'filler' member assignment which expects the record fields and object properties to match up
        /// </summary>
        /// <param name="primaryKey"></param>
        /// <param name="sourceName"></param>
        /// <param name="target"></param>
        /// <param name="sourceField"></param>
        /// <param name="targetField"></param>
        /// <returns></returns>
        private MemberAssignment BuildMemberAssignment(bool primaryKey, string sourceName, Type target,
                                                       string sourceField, string targetField)
        {
            return new MemberAssignment
                       {
                           Nullable = false,
                           Persistable = true,
                           PrimaryKey = primaryKey,
                           Source = new AssignmentPoint(sourceName, sourceField),
                           Target = new AssignmentPoint(target.FullName, targetField)
                       };
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo" /> with the data needed to serialize the target object.
        /// </summary>
        /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to populate with data. </param>
        /// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization. </param>
        /// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("mapLookup", _mapLookup, typeof (Dictionary<Type, List<ISourceMap>>));
        }

        public IRepositoryMapIndex GetRepositoryMapIndex(string repositoryName)
        {
            return new RepositoryMapIndex(this, repositoryName);
        }

        #endregion

    }
}